from text2vec import SentenceModel,cos_sim,BGEModel
import torch
import numpy as np
import pandas as pd
import unionFind 
import re
import dataUtil
from log import log_info
import db
import math
from datetime import datetime
from lsh import RandomHyperplaneLSH,MinHashLSH


import sys
sys.path.append('..')
import configs.config as config

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"

#初始化embedder
embedder = BGEModel(config.model_version)

#生成一个工单编号和文本向量组成的list
def getDataEmbeddings(data_list)->list:
    # embedder = BGEModel(config.model_version)
    data_embeddings_list = []
    #生成一个工单编号和文本向量组成的list
    for data in data_list:
        data_dict_embedding = {}
        data_dict_embedding["id"] = data["id"]
        data_embeddings =  embedder.encode(data["text"])  
        data_dict_embedding["embedding"] = data_embeddings
        # data_addr_embeddings = embedder.encode(data["addr"])  
        # data_dict_embedding["addr"] = data["addr"]
        # data_dict_embedding["addr_embedding"] = data_addr_embeddings
        data_embeddings_list.append(data_dict_embedding)

    return data_embeddings_list

#两两比较向量的相似度(并查集)【旧方法，两两比较速度慢】
def method(data_embeddings_list):
    log_info.info("go in method")
    print("go in method")
    category_dict = {}
    threshold = config.threshold
    uf = unionFind.UnionFind(len(data_embeddings_list))

    for i in range(len(data_embeddings_list)):
        data_i = data_embeddings_list[i]
        embedding_i = data_i["embedding"]
        # addr_i = data_i["addr"]
        # addr_embedding_i = data_i["addr_embedding"]

        for j in range(i+1,len(data_embeddings_list)): 
            if uf.find(i) == uf.find(j):
                continue
            data_j = data_embeddings_list[j]  
            embedding_j = data_j["embedding"]
            # addr_j = data_j["addr"]
            # addr_embedding_j = data_j["addr_embedding"]

            cos_scores_text = cos_sim(embedding_i, embedding_j)[0]
            if cos_scores_text > threshold  :
                uf.union(i,j)
            # 对地址处理的效果都不好【匹配、相似度、混合相似度】，如果想优化，需要地址库，先划分地址，然后再处理相似度

    for i in range(len(data_embeddings_list)):
        uf.record(i)

    log_info.info("number of category:"+str(uf.cnt))
    print("number of category:"+str(uf.cnt))
    # print(uf.groups)

    #将集合中的数据，转换成工单编号
    tmp_num = 0
    for key,value_list in uf.groups.items():
        tmp_num = tmp_num + 1
        category_dict[tmp_num] = []
        for value in value_list:
            category_dict[tmp_num].append(data_embeddings_list[value]["id"])

    return category_dict

#删除单个数据的类别，is_single:是否排除单个数据的类，开启的话，不输出单个数据的类
def deleteSingle(category_dict, is_single: bool)->dict:
    result_category_dict = {}
    if is_single :
        tmp_num = 0
        tmp_category_dict = {}
        for key,value_list in category_dict.items():
            # category_dict.pop(key)
            if len(value_list) < config.control_output_num :
                continue
            tmp_num = tmp_num + 1
            tmp_category_dict[tmp_num] = value_list
        result_category_dict = tmp_category_dict

        log_info.info("After delete singel, number of category:"+str(tmp_num))
        print("After delete singel, number of category:"+str(tmp_num))
    return result_category_dict

#通过excel的方式处理相似的工单
def similarOrder_Excel(input_Excel_path, output_Excel_path, is_single = False):

    data_list,data_dict = dataUtil.readExcel(input_Excel_path)

    data_embeddings_list = getDataEmbeddings(data_list)
    #输出聚类后，包含工单编号的类
    category_dict = method(data_embeddings_list)

    category_dict = deleteSingle(category_dict, is_single)

    dataUtil.writeExcel(output_Excel_path, data_dict, category_dict)

    return 0

#通过数据库的方式处理相似的工单
def similarOrder_DB():

    data_list,data_dict = db.read_mysql()

    data_embeddings_list = getDataEmbeddings(data_list)
    #输出聚类后，包含工单编号的类
    category_dict = method(data_embeddings_list)

    category_dict = deleteSingle(category_dict, config.is_single)

    #输出类别到数据库
    db.write_mysql(data_dict, category_dict)
    return 0

#=====================================================================================================================
#下面接口将加入lsh算法，以加快处理速度【可以修改num_planes，调整速度。num_planes越多划分越细速度越快，但准度会降低】
#加入lsh算法方法，一万数据 时间可以从1.5小时变成10分钟。不过由于平面划分的问题，可能同义工单分到不同的组里，最后需要再做一次聚合

#生成一个超平面作为key划分的字典【LSH版本】
def getDataEmbeddings_LSH(data_list:list, embedder:BGEModel, lsh:RandomHyperplaneLSH):
    #需要返回的值
    key_data_embedding_dict = {}
    data_embedding_dict = {}

    #生成一个超平面作为key划分的字典
    for data in data_list:
        data_dict_embedding = {}
        data_dict_embedding["id"] = data["id"]
        data_embeddings =  embedder.encode(data["text"])  
        data_dict_embedding["embedding"] = data_embeddings
        #用于后面聚合
        data_embedding_dict[data["id"]] = data_embeddings

        #lhs计算哈希
        tmp_lsh_key = lsh.hash(data_embeddings)

        if tmp_lsh_key not in key_data_embedding_dict.keys() :
            key_data_embedding_dict [tmp_lsh_key] = [data_dict_embedding]
        else : 
            key_data_embedding_dict [tmp_lsh_key].append(data_dict_embedding)

    return key_data_embedding_dict, data_embedding_dict

#在划分好的字典里做两两比较【LSH版本】
def method_LSH(key_data_embedding_dict:dict) ->dict:
    threshold = config.threshold
    category_dict = {}      #返回值
    tmp_num = 0

    print("go in method")
    log_info.info("go in method")
    for key,value_list in key_data_embedding_dict.items():
        print("key {}  value num {}".format(str(key),str(len(value_list))))

        if len(value_list) < 2:
            continue

        uf = unionFind.UnionFind(len(value_list))
        
        for i in range(len(value_list)):
            data_i = value_list[i]
            embedding_i = data_i["embedding"]

            for j in range(i+1,len(value_list)): 
                if uf.find(i) == uf.find(j):
                    continue
                data_j = value_list[j]  
                embedding_j = data_j["embedding"]

                cos_scores_text = cos_sim(embedding_i, embedding_j)[0]
                if cos_scores_text > threshold  :
                    uf.union(i,j)

        for i in range(len(value_list)):
            uf.record(i)

        for key,tmp_value_list in uf.groups.items():
            category_dict[tmp_num] = []
            for value in tmp_value_list:
                category_dict[tmp_num].append(value_list[value]["id"])
            tmp_num = tmp_num + 1
            
    print("method end")
    log_info.info("method end")

    return category_dict

#删除单个数据的类别,再做一次聚合，把意思相近的工单聚合到一起【LSH版本】
#is_single:是否排除单个数据的类，开启的话，不会输出单个数据
def deleteSingle_LSH(category_dict: dict, is_single: bool, control_output_num: int)->dict:
    result_category_dict = {}
    if is_single :
        tmp_num = 0
        tmp_category_dict = {}
        for key,value_list in category_dict.items():
            # category_dict.pop(key)
            if len(value_list) < control_output_num :
                continue
            tmp_category_dict[tmp_num] = value_list
            tmp_num = tmp_num + 1

        result_category_dict = tmp_category_dict

        log_info.info("After delete singel, number of category:"+str(tmp_num))
        print("After delete singel, number of category:"+str(tmp_num))
    return result_category_dict

#在筛选后的数据中二次聚合结果
def reunion_LSH(category_dict: dict,data_embedding_dict: dict)->dict:
    threshold = config.reunion_threshold
    result_category_dict = {}
    tmp_num = 0
    uf = unionFind.UnionFind(len(category_dict))
    for i in range(len(category_dict)):
        data_i = category_dict[i][0]
        embedding_i = data_embedding_dict[data_i]

        for j in range(i+1,len(category_dict)): 
            if uf.find(i) == uf.find(j):
                continue
            data_j = category_dict[j][0]  
            embedding_j = data_embedding_dict[data_j]

            cos_scores_text = cos_sim(embedding_i, embedding_j)[0]
            if cos_scores_text > threshold  :
                uf.union(i,j)

    for i in range(len(category_dict)):
        uf.record(i)

    for key,tmp_uf_value_list in uf.groups.items():
        result_category_dict[tmp_num] = []
        for uf_value in tmp_uf_value_list:
            for data_value in category_dict[uf_value]:
                result_category_dict[tmp_num].append(data_value)
        tmp_num = tmp_num + 1
        
    log_info.info("After reunion, number of category:"+str(tmp_num))
    print("After reunion, number of category:"+str(tmp_num))
    return result_category_dict

#通过excel的方式处理相似的工单【LSH版本】
def similarOrder_LSH_Excel(input_Excel_path, output_Excel_path, is_single = False):
    #初始化embedder
    embedder = BGEModel(config.model_version)
    #初始化LSH族 lsh方法一：随机超平面哈希族
    lsh = RandomHyperplaneLSH(num_planes=config.lsh_num_planes, dim=1024)  

    data_list,data_dict = dataUtil.readExcel(input_Excel_path)

    key_data_embedding_dict, data_embedding_dict = getDataEmbeddings_LSH(data_list, embedder, lsh)

    category_dict = method_LSH(key_data_embedding_dict)

    #先删除单个的数据类型
    category_dict = deleteSingle_LSH(category_dict, is_single, 2)

    #聚合已经分好类的群发工单
    category_dict = reunion_LSH(category_dict, data_embedding_dict)

    #显示需要的最少数目
    category_dict = deleteSingle_LSH(category_dict, is_single, config.control_output_num)

    dataUtil.writeExcel(output_Excel_path, data_dict, category_dict)
    return 0


#通过excel的方式处理相似的工单【LSH版本】
def similarOrder_LSH_Excel_api(input_Excel_path, is_single = False):
    #初始化embedder
    # embedder = BGEModel(config.model_version)
    # 读取原始 Excel 文件
    df = pd.read_excel(input_Excel_path)
    # 创建一个空的字典，用于存储筛选后的 DataFrame
    filtered_dfs = {}

    #初始化LSH族 lsh方法一：随机超平面哈希族

    lsh = RandomHyperplaneLSH(num_planes=config.lsh_num_planes, dim=1024)  

    data_list,data_dict = dataUtil.readExcel(input_Excel_path)

    key_data_embedding_dict, data_embedding_dict = getDataEmbeddings_LSH(data_list, embedder, lsh)

    category_dict = method_LSH(key_data_embedding_dict)

    #先删除单个的数据类型
    category_dict = deleteSingle_LSH(category_dict, is_single, 2)

    #聚合已经分好类的群发工单
    category_dict = reunion_LSH(category_dict, data_embedding_dict)

    #显示需要的最少数目
    category_dict = deleteSingle_LSH(category_dict, is_single, config.control_output_num)

    # event,num,gd_ids,keywords = dataUtil.llm_method(category_dict,data_dict)

    # 遍历每个列表，根据列表中的值筛选数据，并存储到字典中的不同 DataFrame 中
    for item in category_dict:
        lst = category_dict[item]
        filtered_dfs[f"df_{item+1}"] = df[df['工单编号'].isin(lst)]
    # 创建一个 Excel writer 对象
    output_file_name = "input/output.xlsx"
    with pd.ExcelWriter(output_file_name) as writer:
        # 将每个 DataFrame 写入 Excel 文件的不同 Sheet 中
        for name, df_i in filtered_dfs.items():
            df_i.to_excel(writer, sheet_name=name, index=False)
    # return event,num,gd_ids,keywords
    return output_file_name    

#通过excel的方式处理相似的工单【LSH版本】
def similarOrder_LSH_Excel_sql(sql_ids, is_single = False):
    #初始化embedder
    # embedder = BGEModel(config.model_version)
    #初始化LSH族 lsh方法一：随机超平面哈希族
    lsh = RandomHyperplaneLSH(num_planes=config.lsh_num_planes, dim=1024)  

    data_list,data_dict = dataUtil.readSql(sql_ids)

    key_data_embedding_dict, data_embedding_dict = getDataEmbeddings_LSH(data_list, embedder, lsh)

    category_dict = method_LSH(key_data_embedding_dict)

    #先删除单个的数据类型
    category_dict = deleteSingle_LSH(category_dict, is_single, 2)

    #聚合已经分好类的群发工单
    category_dict = reunion_LSH(category_dict, data_embedding_dict)

    #显示需要的最少数目
    category_dict = deleteSingle_LSH(category_dict, is_single, config.control_output_num)

    if len(category_dict) == 0:
        for tag, item in enumerate(data_list):
            category_dict[tag] = [item["id"]]

    event,num,gd_ids,keywords = dataUtil.llm_method(category_dict,data_dict)

    return event,num,gd_ids,keywords

# 传入json 输出相同事件 + 归纳总结
# output = f"{num}人反映{event}问题"
def similarOrder_LSH_json(sim_gd, is_single = False):
    
    #初始化LSH族 lsh方法一：随机超平面哈希族
    lsh = RandomHyperplaneLSH(num_planes=config.lsh_num_planes, dim=1024)  

    data_list,data_dict = dataUtil.read_json(sim_gd)

    key_data_embedding_dict, data_embedding_dict = getDataEmbeddings_LSH(data_list, embedder, lsh)

    category_dict = method_LSH(key_data_embedding_dict)

    #先删除单个的数据类型
    category_dict = deleteSingle_LSH(category_dict, is_single, 1)

    #聚合已经分好类的群发工单
    category_dict = reunion_LSH(category_dict, data_embedding_dict)

    #显示需要的最少数目
    category_dict = deleteSingle_LSH(category_dict, is_single, config.control_output_num)

    #输出处理 {0: [1, 2, 4, 5, 3, 6]} 
    # 1、取 字典value 数据最长的 
    # 2、利用大模型事件提取 
    event,num,gd_ids,keywords = dataUtil.llm_method(category_dict,data_dict)

    return event,num,gd_ids,keywords

#通过数据库的方式处理相似的工单【LSH版本】
def similarOrder_LSH_DB():
    #初始化embedder
    embedder = BGEModel(config.model_version)
    #初始化LSH族 lsh方法一：随机超平面哈希族
    lsh = RandomHyperplaneLSH(num_planes=config.lsh_num_planes, dim=1024)  

    data_list,data_dict = db.read_mysql()

    key_data_embedding_dict, data_embedding_dict = getDataEmbeddings_LSH(data_list, embedder, lsh)

    #输出聚类后，包含工单编号的类
    category_dict = method_LSH(key_data_embedding_dict)
    #先删除单个的数据类型
    category_dict = deleteSingle_LSH(category_dict, config.is_single, 2)

    #聚合已经分好类的群发工单
    category_dict = reunion_LSH(category_dict, data_embedding_dict)

    #显示需要的最少数目
    category_dict = deleteSingle_LSH(category_dict, config.is_single, config.control_output_num)

    #输出类别到数据库
    db.write_mysql(data_dict, category_dict)
    return 0

#根据某一条工单查询历史工单中是否有相似的工单API【LSH版本】
#这里只有excel进行测试，所以只写excel版本，数据库版本自行修改
def similarOrder_LSH_Find_Excel_API(order:str, input_Excel_path):
    #初始化embedder
    embedder = BGEModel(config.model_version)
    #初始化LSH族 lsh方法一：随机超平面哈希族
    lsh = RandomHyperplaneLSH(num_planes=config.lsh_num_planes, dim=1024)  

    order_embedding =  embedder.encode(order)  
    order_lsh_key = lsh.hash(order_embedding)

    data_list,data_dict = dataUtil.readExcel(input_Excel_path)

    key_data_embedding_dict,__ = getDataEmbeddings_LSH(data_list, embedder, lsh)

    similarOder_list = []

    if order_lsh_key in key_data_embedding_dict.keys():
        similarOder_list = similarOrder_LSH_Find_Method(order_embedding, key_data_embedding_dict[order_lsh_key], data_dict)

    if len(similarOder_list) == 0 :
        print("NO!!! not find similar order!")
        log_info.info("NO!!! not find similar order!")
    else :
        print("YES!!! find similar order!")
        log_info.info("YES!!! find similar order!")
        print("similar order num:"+str(len(similarOder_list)))
        print(similarOder_list)        

    return 0

#一条工单查询是否有相似的工单核心方法【LSH版本】
def similarOrder_LSH_Find_Method(order_embedding, key_data_embedding_list:list, data_dict:dict) -> list:
    threshold = config.find_similar_threshold
    similarOder_list = []

    for data_dict_embedding in key_data_embedding_list:
        tmp_data_embedding = data_dict_embedding["embedding"]
        cos_scores_text = cos_sim(order_embedding, tmp_data_embedding)[0]
        if cos_scores_text > threshold  :
           similarOder_list.append(data_dict[data_dict_embedding["id"]])

    return similarOder_list

if __name__ == '__main__':
    # similarOrder_Excel(config.input_Excel_path, config.output_Excel_path, is_single = config.is_single)
    now = datetime.now()
    formatted_now = now.strftime("%Y-%m-%d %H:%M:%S")
    print("开始的当前时间:", formatted_now)
    #excel
    # similarOrder_LSH_Excel(config.input_Excel_path, config.output_Excel_path, is_single = config.Is_single)
    # similarOrder_LSH_DB()
    #mysql

    sim_gd = [
        {
            "id":1,
            "addr":"闵行区剑川路201弄144号202室",
            "content":"市民来电反映:市民来电反复催单。催单原因：市民称管理部门至今未给予任何回复，请管理部门尽快处理。【最近派发的工单号：20240109035334，工单内容：重新交办：要求：根据答复要点告知，事实与之不符，其表示根本没有电信的联系市民，陶师傅单子没有结掉，在电信内部网上挂着，没有办法给其更换维修师傅。希望管理部门核实，尽快处理。最近办结的工单编号：20240104009637，内容：市民表示要求高1.9米的师傅上门维修。诉求：希望管理部门核实更换维修师傅。市民来电反映:其是电信宽带，2023年10月17日报修故障，但是一直维修的师傅不来，诉求：要求电信另外安排师傅给其维修。（需回复）】】"
        },
        {
            "id":2,
            "addr":"闵行区剑川路201弄144号202室",
            "content":"市民来电反复催单，要求尽快对其投诉的古师傅和陶师傅予以处理。【最近派发的工单编号：20240105020008，工单内容：重新交办。答复要点与实际情况不符。市民表示从来没有人联系过其解决问题。诉求：请管理部门尽快联系其解决问题。工单内容：重新交办，要求：按答复要点告知市民，市民表示答复要点与实际不符，市民要求管理部门尽快处理市民投诉的古师傅和陶师傅的问题。工单内容：市民来电反映:其是电信宽带，2023年10月17日报修故障，但是一直维修的师傅不来，诉求：要求电信另外安排师傅给其维修。（需回复）】"
        },
        {
            "id":3,
            "addr":"闵行区剑川路201弄144号202室",
            "content":"【重新交办，要求：答复要点与实际情况不符，市民表示没有人联系过市民，也没有人上门，电信宽带依然没有维修，诉求：希望管理部门协调尽快维修。最近办结的工单编号:20240105020008,工单内容：重新交办。答复要点与实际情况不符。市民表示从来没有人联系过其解决问题。诉求：请管理部门尽快联系其解决问题。【】"
        },
        {
            "id":4,
            "addr":"闵行区剑川路201弄144号202室",
            "content":"市民来电反映:市民来电反复催单。催单原因：市民称管理部门至今未给予任何回复，请管理部门尽快处理。【最近派发的工单号：20240109035334，工单内容：重新交办：要求：根据答复要点告知，事实与之不符，其表示根本没有电信的联系市民，陶师傅单子没有结掉，在电信内部网上挂着，没有办法给其更换维修师傅。希望管理部门核实，尽快处理。最近办结的工单编号：20240104009637，内容：市民表示要求高1.9米的师傅上门维修。诉求：希望管理部门核实更换维修师傅。市民来电反映:其是电信宽带，2023年10月17日报修故障，但是一直维修的师傅不来，诉求：要求电信另外安排师傅给其维修。（需回复）】】"
        },
        {
            "id":5,
            "addr":"闵行区剑川路201弄144号202室",
            "content":"市民来电反复催单，要求尽快对其投诉的古师傅和陶师傅予以处理。【最近派发的工单编号：20240105020008，工单内容：重新交办。答复要点与实际情况不符。市民表示从来没有人联系过其解决问题。诉求：请管理部门尽快联系其解决问题。工单内容：重新交办，要求：按答复要点告知市民，市民表示答复要点与实际不符，市民要求管理部门尽快处理市民投诉的古师傅和陶师傅的问题。工单内容：市民来电反映:其是电信宽带，2023年10月17日报修故障，但是一直维修的师傅不来，诉求：要求电信另外安排师傅给其维修。（需回复）】"
        },
        {
            "id":6,
            "addr":"闵行区剑川路201弄144号202室",
            "content":"【重新交办，要求：答复要点与实际情况不符，市民表示没有人联系过市民，也没有人上门，电信宽带依然没有维修，诉求：希望管理部门协调尽快维修。最近办结的工单编号:20240105020008,工单内容：重新交办。答复要点与实际情况不符。市民表示从来没有人联系过其解决问题。诉求：请管理部门尽快联系其解决问题。【】"
        }
    ]
    event,num,gd_ids,keywords = similarOrder_LSH_json(sim_gd,is_single = config.Is_single)
    # similarOrder_LSH_Find_Excel_API("上海市浦东新区东力新村9885弄【随申拍】上海市浦东新区申江路东力新村门口9885路路边无证经营设摊影响交通通行，大约摆摊五六个每天摆摊时间下午6：00左右开始要求相关部门尽快处理恢复交通秩序，否则我天天投诉[随申拍问题分类:随申拍]", config.input_Excel_path)

    now = datetime.now()
    formatted_now = now.strftime("%Y-%m-%d %H:%M:%S")
    print("结束的当前时间:", formatted_now)
